package parsetree;

import static util.ArrayConcat.concatenate;

import java.awt.Color;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.event.DocumentEvent.ElementChange;
import javax.swing.text.AbstractDocument.ElementEdit;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

import metagrammar.Operator;
import metagrammar.Operator.Instance;
import metagrammar.Type;
import parsetree.exceptions.DocumentContractBrokenException;
import parsetree.exceptions.UnableToUpdateParseTreeException;

import com.google.common.collect.ImmutableList;

public class DefaultOperatorElement extends AbstractBranchElement implements Verifiable, OperatorElement {
    
    private static final AttributeSet DEFAULT_STYLE;
    
    static {
        SimpleAttributeSet attributes = new SimpleAttributeSet();
        attributes.addAttribute(StyleConstants.Foreground, Color.BLACK);
        DEFAULT_STYLE = attributes;
    }
    
    public static final String NAME = "operator";
    
    private final OperatorSpec spec;
    
    private final OperatorLiteral operatorLiteral;
    
    public DefaultOperatorElement(BranchElement parent, OperatorSpec operatorSpec,
            int operatorStart, int operatorEnd) throws BadLocationException
            {
        super(parent, new ArrayList<Element>(operatorSpec.getOperator().getInputTypes().size()));
        
        this.spec = operatorSpec;
        operatorLiteral = new OperatorLiteral(this, operatorStart, operatorEnd);
        addChild(operatorLiteral);
            }
    
    private DefaultOperatorElement(DefaultOperatorElement operatorElement, ArrayList<Element> children)
    {
        super(operatorElement.getParent(), children);
        this.spec = operatorElement.spec;
        this.operatorLiteral = operatorElement.operatorLiteral;
    }
    
    private DefaultOperatorElement(DefaultOperatorElement operatorElement)
    {
        super(operatorElement.getParent(), operatorElement.getChildren());
        this.spec = operatorElement.spec;
        this.operatorLiteral = operatorElement.operatorLiteral;
    }
    
    /**
     * Adds a child to this, as per {@link AbstractBranchElement#addChild(Element)},
     * iff the child can be added via {@link #addChild(OperatorElement)} of if
     * the Element is the OperatorLiteral for this.
     * @see {@link AbstractBranchElement#addChild(Element)} 
     */
    @Override
    public ElementEdit addChild(Element elem)
    {
        //FIXME
        if(elem instanceof OperatorElement) {
            return addChild((OperatorElement) elem);
        }
        else if(elem == operatorLiteral) {
            super.addChild(operatorLiteral);
        }
        return null;
    }
    
    /**
     * Add a tree rooted in a {@link OperatorElement} as a child of this.
     * The child will only be added if the {@link Type} expressed by the
     * {@link OperatorElement} (ie: returned by {@link OperatorElement#getType()})
     * is a subtype of the {@link Type} required in that position in the {@link OperatorSpec} 
     * @param opElem the {@link OperatorElement} to be added.
     * @return true if opElem is now a child of this, false otherwise.
     * @see {@link AbstractBranchElement#addChild(Element)}
     */
    public ElementEdit addChild(OperatorElement opElem)
    {
        int tupleIndex = getIndexOfChildStartsWith(opElem.getStartOffset());
        if(tupleIndex < 0) {
            tupleIndex = (-tupleIndex) - 1;
        }
        if(opElem.getStartOffset() >= getOperatorLiteral().getEndOffset()) {
            tupleIndex -= 1;
        }
        //TODO
        List<Type> inputTuple = spec.getCorrectedInputTuple();
        if(inputTuple.size() > tupleIndex
                && inputTuple.get(tupleIndex).equals(opElem.getType())) {
            return super.addChild(opElem);
        }
        else {
            return null;
        }
    }
    
    @Override
    public Type getType()
    {
        return getOperator().getExpressedType();
    }
    
    @Override
    public String getName()
    {
        return NAME;
    }
    
    @Override
    public Operator getOperator()
    {
        return spec.getOperator();
    }
    
    public int[] getCorrectedInputTupleOrder()
    {
        return spec.getCorrectedInputTupleOrder();
    }
    
    @Override
    public Instance getOperatorInstance()
    {
        int numberChildren = getChildCount();
        metagrammar.Operator.Instance[] inputTuple = new metagrammar.Operator.Instance[numberChildren - 1];
        int literalIndex = getIndexOfChildStartsWith(operatorLiteral.getStartOffset());
        for(int i = 0; i < literalIndex; i++) {
            Element child = getElement(i);
            if(child instanceof DefaultOperatorElement) {
                inputTuple[i] = ((DefaultOperatorElement) child).getOperatorInstance();
            }
            else {
                return null;
            }
            
        }
        for(int i = literalIndex + 1; i < numberChildren; i++) {
            Element child = getElement(i);
            if(child instanceof DefaultOperatorElement) {
                inputTuple[i - 1] = ((DefaultOperatorElement) child).getOperatorInstance();
            }
            else {
                return null;
            }
        }
        metagrammar.Operator.Instance[] correctedInputTuple = new metagrammar.Operator.Instance[inputTuple.length];
        int[] order = getCorrectedInputTupleOrder();
        for(int i = 0; i < correctedInputTuple.length; i++) {
            correctedInputTuple[order[i]] = inputTuple[i];
        }
        return getOperator().getInstance(correctedInputTuple);
    }
    
    private OperatorLiteral getOperatorLiteral()
    {
        return operatorLiteral;
    }
    
    @Override
    public Element[] split(int modelOffset) throws BadLocationException
    {
        if(modelOffset <= getStartOffset()) {
            throw new BadLocationException(String.format(
                    "Model offset at %d is at or before start of element in split.", modelOffset),
                    modelOffset);
        }
        if(modelOffset >= getEndOffset()) {
            throw new BadLocationException(String.format(
                    "Model offset at %d or after end of element in split.", modelOffset),
                    modelOffset);
        }
        
        // Retrieves the index of the child that needs to be split, if it exists.
        int splitIndex = getIndexOfChildContaining(modelOffset);
        final ArrayList<Element> firstChildren;
        final ArrayList<Element> secondChildren;
        final DefaultOperatorElement operatorElement;
        final Element[] splitElement;
        ImmutableList<Element> children = getChildren();
        if(splitIndex >= 0) {
            // if (splitIndex >= 0) a child will need to be split.
            Element child = getElement(splitIndex);
            firstChildren = new ArrayList<Element>(children.subList(0, splitIndex));
            secondChildren = new ArrayList<Element>(children.subList(splitIndex + 1,
                    children.size()));
            Element[] firstArray, split, thirdArray;
            if(child == operatorLiteral) {
                // if the operatorLiteral is split this OperatorElement is invalid
                // Propagate this Element's children
                firstArray = firstChildren.toArray(new Element[] {});
                split = child.split(modelOffset);
                thirdArray = secondChildren.toArray(new Element[] {});
            }
            else if(child.getStartOffset() < operatorLiteral.getStartOffset()) {
                // Otherwise only the children beyond the split must be propagated
                firstArray = firstChildren.toArray(new Element[] {});
                split = child.split(modelOffset);
                operatorElement = new DefaultOperatorElement(this, secondChildren);
                thirdArray = new Element[] { operatorElement };
            }
            else {
                operatorElement = new DefaultOperatorElement(this, firstChildren);
                firstArray = new Element[] { operatorElement };
                split = child.split(modelOffset);
                thirdArray = secondChildren.toArray(new Element[] {});
            }
            splitElement = concatenate(firstArray, split, thirdArray);
        }
        else {
            // if (splitIndex < 0) the split occurred between children.
            int correctedIndex = (-splitIndex) - 1;
            firstChildren = new ArrayList<Element>(
                    children.subList(0, correctedIndex));
            secondChildren = new ArrayList<Element>(children.subList(correctedIndex,
                    children.size()));
            if(operatorLiteral.getStartOffset() > modelOffset) {
                // operatorLiteral is part of the second set of children
                operatorElement = new DefaultOperatorElement(this, secondChildren);
                splitElement = firstChildren.toArray(new Element[firstChildren.size() + 1]);
                splitElement[firstChildren.size()] = operatorElement;
            }
            else {
                operatorElement = new DefaultOperatorElement(this, firstChildren);
                Element[] second = secondChildren.toArray(new Element[secondChildren.size()]);
                splitElement = concatenate(new Element[] { operatorElement }, second);
            }
        }
        return splitElement;
    }
    
    @Override
    public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
    {
        try {
            Element[] update;
            if(change.getOffset() > getStartOffset() && change.getOffset() < getEndOffset()) {
                update = split(change.getOffset());
            }
            else if(change.getOffset() + change.getLength() > getStartOffset()
                    && change.getOffset() + change.getLength() < getEndOffset()) {
                update = split(change.getOffset() + change.getLength());
            }
            else {
                // TODO
                return null;
            }
            BranchElement parent = this;
            ElementEdit addition = null;
            while(addition == null && parent != null) {
                parent = parent.getParent();
                addition = parent.addChildren(update, true, true);
            }
            if(addition != null) {
                List<ElementChange> changes = new LinkedList<ElementChange>();
                changes.add(addition);
                changes.addAll(parent.insertUpdate(change, attributes));
                return changes;
            }
            return null;
        }
        catch(BadLocationException e) {
            throw new DocumentContractBrokenException(e);
        }
    }
    
    @Override
    public List<ElementChange> postRemoveUpdate(Removal change)
    {
        LinkedList<ElementChange> changes = new LinkedList<ElementChange>();
        // a removal from an operator element likely invalidates it or one of its children
        final Element[] elems;
        int length = operatorLiteral.getEndOffset() - operatorLiteral.getStartOffset();
        if(length != spec.getLiteral().length()) {
            // if the operatorLiteral is no longer the correct length this operator is invalid
            if(getChildCount() > 1 || length > 0) {
                // if there are other children besides the OperatorLiteral or the OperatorLiteral isn't empty
                // we need to add children to one of the parents
                int index = getIndexOfChildStartsWith(operatorLiteral.getStartOffset());
                if(length == 0) {
                    // if the OperatorLiteral is empty simply remove it
                    changes.add(removeEmptyChild(operatorLiteral));
                    elems = getChildren().toArray(new Element[] {});
                }
                else {
                    // otherwise replace it with a ParsingElement
                    elems = getChildren().toArray(new Element[] {});
                    try {
                        ParsingElement elem = new ParsingElement(getParent(),
                                operatorLiteral.getStartOffset(),
                                operatorLiteral.getEndOffset());
                        elems[index] = elem;
                    }
                    catch(BadLocationException e) {
                        throw new DocumentContractBrokenException(e);
                    }
                }
                boolean added = false;
                BranchElement parent = this;
                ElementEdit addition = null;
                while(parent != null && addition == null) {
                    // find a parent in which the ParsingElements can be added.
                    parent = parent.getParent();
                    addition = parent.addChildren(elems, true, true);
                }
                if(addition == null) {
                    throw new UnableToUpdateParseTreeException(
                    "No parent of a OperatorElement can accept Untyped Elements");
                }
                changes.add(addition);
                changes.addAll(parent.postRemoveUpdate(change));
                return changes;
            }
            else {
                // if this has no children it is removed
                changes.add(getParent().removeEmptyChild(this));
                return changes;
            }
        }
        else {
            // if this operator is valid we still need to check its children
            return postRemoveUpdateChildren(change);
        }
    }
    
    @Override
    public Element merge(Element element)
    {
        if(element instanceof DefaultOperatorElement) {
            DefaultOperatorElement opElement = (DefaultOperatorElement) element;
            if(opElement.spec.getPrecedence() > spec.getPrecedence()) {
                return handleMerge(opElement, this, true);
            }
            else if(opElement.spec.getPrecedence() < spec.getPrecedence()) {
                return handleMerge(this, opElement, true);
            }
            else if(getEndOffset() <= element.getStartOffset()) {
                return handleMerge(this, opElement, true);
            }
            else {
                return handleMerge(opElement, this, true);
            }
        }
        return null;
    }
    
    private Element handleMerge(DefaultOperatorElement highPrecedenceElement,
            DefaultOperatorElement lowPrecedenceElement, boolean firstAttempt)
    {
        int childIndex;
        int size;
        // decide if the highPrecedenceElement is before or after the operatorLiteral of the lowPrecedenceElement
        if(highPrecedenceElement.getStartOffset() >= lowPrecedenceElement.getOperatorLiteral()
                .getEndOffset()) {
            childIndex = lowPrecedenceElement.getIndexOfChildEndsWith(lowPrecedenceElement
                    .getOperatorLiteral().getEndOffset());
            size = lowPrecedenceElement.getChildCount();
        }
        else if(highPrecedenceElement.getEndOffset() <= lowPrecedenceElement
                .getOperatorLiteral().getStartOffset()) {
            childIndex = 0;
            size = lowPrecedenceElement.getIndexOfChildStartsWith(lowPrecedenceElement.getOperatorLiteral()
                    .getStartOffset());
        }
        else {
            return null;
        }
        
        DefaultOperatorElement highClone = new DefaultOperatorElement(highPrecedenceElement);
        // see if the OperatorElement with higher precedence can merge with the children of the one with lower precedence
        for(; childIndex < size; childIndex++) {
            Element merge = lowPrecedenceElement.getElement(childIndex).merge(highClone);
            if(merge != null) {
                // if it can, see if the merged Element can be added to the lower precedence OperatorElement.
                DefaultOperatorElement lowClone = new DefaultOperatorElement(lowPrecedenceElement);
                if(lowClone.addChild(merge) != null) {
                    // if it can, the merge succeeded.
                    return lowClone;
                }
            }
        }
        // if the higher precedence OperatorElement can't be merged with any of the children of the lower,
        // try to add the higher to the lower.
        highClone = new DefaultOperatorElement(highPrecedenceElement);
        DefaultOperatorElement lowClone = new DefaultOperatorElement(lowPrecedenceElement);
        if(lowClone.addChild(highClone) != null) {
            return lowClone;
        }
        else if(firstAttempt) {
            // if all else fails, try reversing the precedences and merging again 
            return handleMerge(lowPrecedenceElement, highPrecedenceElement, false);
        }
        // all attempts to merge failed merge is impossible.
        return null;
    }
    
    @Override
    public boolean verify()
    {
        //FIXME
        Operator operator = getOperator();
        ImmutableList<Type> requiredTuple = operator.getInputTypes();
        int numberChildren = getChildCount();
        if(numberChildren != requiredTuple.size() + 1) {
            return false;
        }
        int literalIndex = getIndexOfChildStartsWith(operatorLiteral.getStartOffset());
        for(int i = 0; i < literalIndex; i++) {
            Element child = getElement(i);
            if(child.getType() != requiredTuple.get(i)) {
                return false;
            }
        }
        for(int i = literalIndex + 1; i < numberChildren; i++) {
            Element child = getElement(i);
            if(child.getType() != requiredTuple.get(i - 1)) {
                return false;
            }
        }
        return verifyChildren();
    }
    
    private boolean verifyChildren()
    {
        int numberChildren = getChildCount();
        for(int i = 0; i < numberChildren; i++) {
            Element child = getElement(i);
            if(child instanceof Verifiable && !((Verifiable) child).verify()) {
                return false;
            }
        }
        return true;
    }
    
    protected class OperatorLiteral extends AbstractLeafElement {
        
        public static final String NAME = "operator-literal";
        
        private final int length;
        
        public OperatorLiteral(AbstractBranchElement parent, int startOffset, int endOffset)
        throws BadLocationException
        {
            super(parent, startOffset);
            length = endOffset - startOffset;
            //setAttributes(DEFAULT_STYLE);
            getDocument().setCharacterAttributes(startOffset, length, DEFAULT_STYLE, true);
        }
        
        @Override
        public Type getType()
        {
            return Type.UNTYPED;
        }
        
        @Override
        public String getName()
        {
            return NAME;
        }
        
        @Override
        public Element merge(Element element)
        {
            return null;
        }
        
        @Override
        public List<ElementChange> insertUpdate(Insert change, AttributeSet attributes)
        {
            // nonaccessible
            return null;
        }
        
        @Override
        public List<ElementChange> postRemoveUpdate(Removal change)
        {
            // nonaccessible
            return null;
        }
        
        @Override
        public int getEndOffset()
        {
            return getStartOffset() + length;
        }
        
    }
}